VPR-64 feat(phone): schoolwide and unit phone lists - #323
Conversation
Bundle ReportChanges will increase total bundle size by 999 bytes (0.04%) ⬆️. This is within the configured threshold ✅ Detailed changes
Affected Assets, Files, and Routes:view changes for bundle: viper-frontend-esmAssets Changed:
Files in
|
|
@coderabbitai full review |
|
There was a problem hiding this comment.
Pull request overview
This PR migrates the schoolwide (SVM) and Dean's Office (VMDO) phone lists from Viper 1 into a new Personnel area, backed by a new normalized phones schema in the VIPER database. Viewing requires basic SVMSecure permission, while a new SVMSecure.PhoneLists.SVMMaintain permission (and per-list MaintainRole) gates editing. It adds EF Core models/services/controllers plus a full Vue 3/Quasar SPA, and refactors shared person-search logic into a reusable PersonSearchHelper used by both CMS and Personnel.
Changes:
- New
phonesschema +PhonesDbContext, EF models, area services and/api/phones/...controllers with dynamic per-list maintain permissions and direct-number masking. - New Personnel Vue SPA (lists, maintenance views, person selector, record dialogs) plus data-migration scripts from the legacy PhoneList database.
- Shared
PersonSearchHelperextracted and adopted by CMS'sSearchPeople, forcing EF parameterization (ESCAPE clause) to prevent per-term query plans and%/_wildcard injection.
Reviewed changes
Copilot reviewed 121 out of 122 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| web/Viper.csproj | Excludes Areas\Personnel\Scripts\** (separate migration project) from the web build, mirroring the Effort area. |
| web/Program.cs | Registers PhonesDbContext, adds Personnel SPA name and the Personnel services namespace to Scrutor registration. |
| web/Classes/Utilities/PersonSearchHelper.cs | New shared expression-tree helper for name-search autocomplete with parameterized Contains matching. |
| web/Areas/Personnel/Services/PhoneSVMSectionService.cs | Read-only query for SVM sections, ordered with null-safe sort. |
| web/Areas/Personnel/Services/PhoneSVMFrequentNumberService.cs | CRUD + soft-delete for SVM frequent numbers, with modified-date tracking. |
| web/Areas/Personnel/Services/PhonePersonLookupService.cs | Looks up phone people by IAM IDs (direct number masked unless maintainer) and current-employee search. |
| web/Areas/Personnel/Services/PhonePermissionsService.cs | Resolves edit permission from the list's MaintainRole column. |
| web/Areas/Personnel/Controllers/PhonePersonController.cs | Person-picker endpoint merging Viper and phone data; uses foreach/Add where .Select() is preferred. |
| web/Areas/Personnel/Controllers/PhoneSVMModifiedDateController.cs | Returns latest SVM modified date; contains a comment typo ("Identfies"). |
| web/Areas/Personnel/Models/*, VueApp/src/Personnel/** | New EF models/DTOs/Mapperly mapper and the Personnel Vue SPA (services, composables, components, tests). |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| List<string> iamIds = []; | ||
| foreach (ViperPerson result in viperResults) | ||
| { | ||
| iamIds.Add(result.IamId); | ||
| } |
| private readonly PhoneSVMUnitService _phoneSVMUnitService = phoneSVMUnitService; | ||
|
|
||
| /// <summary> | ||
| /// Identfies when frequent numbers were last modified. |
rlorenzo
left a comment
There was a problem hiding this comment.
Solid work, and the parts that are easy to get wrong are right: ResolveListForMaintain, VerifyUnitInList, and GetUnitPersonInList each re-scope by list rather than trusting the id in the request, with a test proving one list's role grants nothing on another. I ran the branch against dev, so the inline notes are reproductions. Four things block deployment, none of them in the code:
- The DDL won't run.
CREATE SCHEMA Inventory;should bephones, so everyCREATE TABLE [phones].[...]fails. FourALTER TABLE [phones].[SVMUnitPerson] CHECK CONSTRAINTstatements also name the wrong table, and three run before that table exists. - The DDL is missing the unique index on
PhoneList.Code, and dev already has it.UX_PhoneList_Codewas added to dev by hand, so Production won't get it and a duplicate code would resolve arbitrarily, including for the permission check. - A permission is missing from the steps. VMDO's MaintainRole is
SVMSecure.PhoneLists.VMDOMaintain, but onlySVMMaintainis listed, so nobody could maintain VMDO. - The pages aren't reachable from the nav.
MainNav.cs:29andMiniNav/Default.cshtml:105-109still point Personnel at VIPER 1, thoughApp.vuesetshighlighted-top-nav="Personnel".
Also Home.vue needs a personnel-home CMS record per environment, or redirected non-maintainers land on a blank page. Everything else is inline, tagged minor where it's a nit rather than a fix I'd hold the PR for.
| dense | ||
| outlined | ||
| label="Location" | ||
| maxlength="100" |
There was a problem hiding this comment.
Writes to SVMUnitPerson.Office, which is NVARCHAR(50), so 51 to 100 chars 500s ("String or binary data would be truncated"). Needs maxlength="50" plus server-side length validation on the DTOs.
| await _phoneSVMUnitService.AddOrUpdateUnitData(unitId, request, ct); | ||
| return Ok(true); | ||
| } | ||
| catch (InvalidOperationException ex) |
There was a problem hiding this comment.
Only InvalidOperationException is caught, here and in every controller in the area, so DbUpdateException/SqlException become 500s. Catching DbUpdateException and mapping it to a 400 would cover the class.
| /// </summary> | ||
| public async Task AddUnitPersonData(int listId, PhoneListUnitDataRequest request, CancellationToken ct = default) | ||
| { | ||
| await VerifyUnitInList(listId, request.UnitId, ct); |
There was a problem hiding this comment.
request.EmployeeIam is never checked against users.Person, and no FK can enforce it since IamId isn't unique, so an unknown IAM returns 200 and writes a row that never renders. Needs an existence check before insert.
| // Avoid returning direct numbers except to users with permissions to access them. | ||
| // This data should only be returned for queries tied to a list for which the user | ||
| // has maintain permissions. | ||
| bool canAccessDirectNumber = list != null && _phonePermissionsService.CanMaintainList(list); |
There was a problem hiding this comment.
The gate checks you maintain listCode, but the results aren't scoped to that list, so a maintainer of list A gets DirectPhone for people only on list B. Scope the projection to list's members.
There was a problem hiding this comment.
The current behavior is the desired/required one, but this case is a bit complicated.
Anyone with the ability to maintain any unit-specific list has the ability to add any user to it and so can gain access to the data anyways - scoping to the list doesn't actually achieve anything. Auto-populating direct phone data into the form for new additions to the list is dependent on this behavior, and users existing as part of multiple groups is viable (some admin staff already span multiple units) so auto-populating existing data to avoid overwriting existing values is important.
Ideally, we'd limit people returned by this query to those who have an appointment in the relevant unit, but Brandon mentioned that some units (including VMDO) do not fit nicely into the existing UCPath data, so this isn't viable with the current data quality.
What's most important is to not return the data to just anyone, or as part of the SVM queries where it is never needed.
I'll change the comment to be a bit more precise instead.
| } | ||
|
|
||
| function reportSaveError(res: { errors: string[] | null }) { | ||
| formError.value = res.errors?.[0] ?? `Failed to ${isEdit.value ? "save" : "upload"} ${recordLabel}` |
There was a problem hiding this comment.
errors[0] is EF's wrapper message and the useful one is last, so the truncation failure shows "See the inner exception for details" instead of the reason. Take the last entry, or join.
There was a problem hiding this comment.
I can make this change, but all 25 other references to res.errors in the repo use res.errors?.[0] ?? for error reporting, and the errors reported here seem to be correct for both 4xx and 5xx errors. Is this a systematic issue across the repo?
| okColor: "negative", | ||
| }) | ||
| if (!confirmed) return | ||
| let isError: boolean = false |
There was a problem hiding this comment.
Minor: isError plus let r does what the if/else in SVMPhonesMaintain.deleteRecord does directly. const either way.
| /// in that unit. | ||
| /// </summary> | ||
| [HttpGet("units")] | ||
| public async Task<ActionResult<List<PhoneListUnit>>> GetUnits(string code, CancellationToken ct = default) |
There was a problem hiding this comment.
Minor: returning the EF entity leaks the model into the API, so the TS types carry always-null nav props (phoneListUnit: null, unitPersons: null). PersonnelMapper is already here for AugmentedViperPerson.
|
|
||
| namespace Viper.Areas.Personnel.Services | ||
| { | ||
| public class PhonesPermissionsService( |
There was a problem hiding this comment.
Minor: file is PhonePermissionsService.cs, class is PhonesPermissionsService.
| const rows: PhoneListDisplayRecord[] = [] | ||
| const cols: QTableProps["columns"] = [ | ||
| { name: "name", label: "Name", field: "name", align: "left", sortable: true }, | ||
| { name: "phone", label: "Phone", field: "phone", align: "left", sortable: false }, |
There was a problem hiding this comment.
Minor: phone, direct phone, office, and fax are all sortable: false while the text columns sort, though the description says the lists are sortable now. Same in svm-data-fetch.ts:76.
There was a problem hiding this comment.
This one is intentional, since sorting by these fields is not meaningful to end users. I'll update the description to clarify.
| </template> | ||
|
|
||
| <script setup lang="ts"> | ||
| import { searchPeopleOptions } from "../services/phone-person-options-service" |
There was a problem hiding this comment.
Minor: this component is ~85% identical to CMS/components/PersonSelector.vue. The search logic came out into use-person-search correctly; the component could follow with a couple of props.
This PR migrates the schoolwide and Dean's Office phone lists from Viper 1. Viewing the lists requires only basic permissions, while specific permissions allow users to edit and maintain the lists. The lists are now housed in the new Personnel area.
The migration makes the following functional changes from the Viper 1 version:
This PR also does some refactoring around Person selection and dialog boxes. There should be no end user impact to CMS, but a few files are affected.
This PR requires schema changes to the Production database:
This PR requires creating a new permission on Production: SVMSecure.PhoneLists.SVMMaintain.
This PR requires running the migration script
.\RunMigrateData.bat Productionfor a dry run, and then.\RunMigrateData.bat Production --applyto migrate data into the new schema.